<?php

namespace Yolo\Service;

use Hyperf\Stringable\Str;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use Yolo\Di\Annotations\Singleton;
use Yolo\Di\DI;
use Yolo\Di\Errors\CircularDependencyException;
use Yolo\Di\Errors\InvalidAttributeException;
use Yolo\Di\Errors\ParameterTypeEmptyException;
use Yolo\Errors\EndpointNotFoundException;
use Yolo\Rpc\Annotations\Api;
use Yolo\Rpc\Annotations\AppScheme;
use Yolo\Rpc\Annotations\Restful;
use Yolo\Rpc\Annotations\RpcController;
use Yolo\Rpc\Annotations\Tunnel;
use Yolo\Utils\Console;
use Yolo\Utils\Path;

#[Singleton]
class RpcApiManager
{

    private array $apis = [];

    /**
     * 获取所有endpoint
     * @return array
     */
    public function getEndpoints(): array
    {
        return $this->apis;
    }

    /**
     * @param AppScheme $appScheme
     * @param ReflectionClass $reflection
     * @return void
     * @throws ReflectionException
     * @throws CircularDependencyException
     * @throws ParameterTypeEmptyException|InvalidAttributeException
     */
    public function getRpcApi(AppScheme $appScheme, ReflectionClass $reflection): void
    {
        $controller = $reflection->getName();

        if (!$this->isRpcController($reflection)) {

            Console::log("RpcApiManager: Class $controller has no RpcController attribute, ignored.", 'warn');
            return;
        }

        $this->apis = array_merge($this->apis, $this->loadControllerEndpoints($appScheme, $reflection));

        $this->newInstance($reflection->getName());
    }

    /**
     * 获取类接口
     * @param AppScheme $appScheme
     * @param ReflectionClass $reflection
     * @return array
     */
    private function loadControllerEndpoints(AppScheme $appScheme, ReflectionClass $reflection): array
    {
        $apis = [];

        $serviceName = $appScheme->getServiceName();
        $className = $reflection->getName();

        foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            $attributes = $method->getAttributes();
            foreach ($attributes as $attribute) {

                if (!$this->isApi($attribute->getName())) {
                    continue;
                }

                $attributeName = $attribute->getName();
                $arguments = $attribute->getArguments();
                $name = $method->getName();
                $apiName = ucfirst($name);
                $options = $this->getApiOptions($attributeName, $arguments);

                $endpoint = $this->buildEndpoint($options['service'] ?? $serviceName, $attributeName, $apiName);

                $apiType = Str::afterLast($attributeName, '\\');

                $api = [
                    'type' => $apiType,
                    'endpoint' => $endpoint,
                    'invoke' => [
                        'controller' => $className,
                        'action' => $name,
                    ],
                    'options' => $options,
                ];

                if ($apiType === 'Restful') {

                    $path = Path::formatPath($arguments[1]);
                    $api['method'] = $arguments[0];
                    $api['path'] = $path;
                    $api['pathParsed'] = Path::parseUri($path);

                    Console::log("RpcApiManager: Loaded RPC Restful API: \"$endpoint\".");
                } else {

                    $apiTypeName = $attributeName === Api::class ? 'RPC API' : 'RPC Tunnel API';

                    Console::log("RpcApiManager: Loaded $apiTypeName: \"$endpoint\".");
                }

                $apis[$endpoint] = $api;
            }
        }

        return $apis;
    }

    private function isApi(string $attributeName): bool
    {
        return in_array($attributeName, [
            Api::class,
            Tunnel::class,
            Restful::class,
        ]);
    }

    private static function getControllerAbsolutePath(string $type): string
    {
        return BASE_PATH . "/app/Controller/$type";
    }

    private function isRpcController(ReflectionClass $reflection): bool
    {
        return array_filter($reflection->getAttributes(), function ($attribute){

            return $attribute->getName() === RpcController::class;
        }) !== [];
    }

    /**
     * @param string $endpoint
     * @return array
     * @throws EndpointNotFoundException
     */
    public function getEndpoint(string $endpoint): array
    {

        if (!isset($this->apis[$endpoint])) {
            throw new EndpointNotFoundException("Endpoint $endpoint not found.");
        }

        return $this->apis[$endpoint];
    }

    /**
     * Set singleton for rpc controller.
     * @param string $className
     * @return void
     * @throws ReflectionException
     * @throws CircularDependencyException
     * @throws ParameterTypeEmptyException|InvalidAttributeException
     */
    private function newInstance(string $className): void
    {
        DI::instance($className, DI::use($className));
    }

    /**
     * Get rpc api type.
     * @param string $attribute
     * @return string
     */
    private function getRpcApiType(string $attribute): string
    {
        return match ($attribute) {
            Tunnel::class => 'Tunnel',
            Restful::class => 'Restful',
            default => 'Api',
        };
    }

    /**
     * Build rpc endpoint.
     * @param string $service
     * @param string $attribute
     * @param string $api
     * @return string
     */
    private function buildEndpoint(string $service, string $attribute, string $api): string
    {
        $apiType = $this->getRpcApiType($attribute);

        return $service . ($apiType === 'Api' ? '' : ".$apiType") . '.' . $api;
    }

    /**
     * Get api options.
     * @param string $attribute
     * @param array $arguments
     * @return array
     */
    private function getApiOptions(string $attribute, array $arguments): array
    {
        if ($attribute === Restful::class) {
            return $arguments[2] ?? [];
        }

        return $arguments[0] ?? [];
    }
}
